終於到了專案練習的部分,這次的專案是要做一個字幕搜尋的API,這個API可以透過關鍵字搜尋字幕,並且可以透過API取得字幕的內容。這個專案會分成四天,第一天的任務是將字幕資料導入到資料庫中。
為了著重於Go語言實作的部份以及節省時間,這邊我的資料會先拿別人已經整理好的資料(MYGO 網站時間軸更新 @場外休憩區 哈啦板 - 巴哈姆特),不過基本上只要確定你的字幕資料中有包含字幕的內容、影片集數、開始時間、結束時間等資訊即可。
mygo
├── data
│ ├── data.go
│ ├── db.go
│ ├── go.mod
│ └── go.sum
├── data.json
├── go.mod
├── go.sum
└── main.go
首先建立mygo
資料夾,並運行go mod init mygo
初始化module,然後在mygo
資料夾中建立data
資料夾,並在data
資料夾中初始化module為mygo/data
,接著在mygo
資料夾的go.mod
中加入replace
來指定data
資料夾的module。初始化結束後將你的資料與main.go放在相同的資料夾中
module mygo
go <current version>
replace mygo/data => ./data
require mygo/data v0.0.0
{
"result": [
{
"episode": "1",
"start": 24,
"end": 48,
"text": "text1"
"segment_id": 1
}
// ...
]
}
CREATE TABLE sentence (
id INT PRIMARY KEY AUTO_INCREMENT,
text TEXT NOT NULL,
episode VARCHAR(3) NOT NULL,
frame_start INT NOT NULL,
frame_end INT NOT NULL,
segment_id INT NOT NULL UNIQUE
);
我這邊的資料是一個JSON中包著list of object, 每個object中包含了影片集數、開始時間、結束時間、字幕內容等資訊,其中開始時間和結束時間以frame為單位。如果你的資料格式跟我這邊不一樣,請自行修改code或資料格式
首先我們先建立一個資料夾data
, 接著透過go mod init
初始化module
mkdir data
go mod init data
接著我們將資料處理成list of struct的格式,這樣之後才能將資料寫入到資料庫中。資料庫的部份我們透過gorm來處理,所以我們可以在定義JSON struct同時定義資料庫的struct
package data
import (
"encoding/json"
"os"
)
type SentenceItem struct {
Text string `json:"text" gorm:"type:text;not null"`
Episode string `json:"episode" gorm:"type:varchar(3);not null"`
FrameStart int `json:"frame_start" gorm:"not null"`
FrameEnd int `json:"frame_end" gorm:"not null"`
SegmentId int `json:"segment_id" gorm:"primaryKey"`
}
type Sentence struct {
Result []SentenceItem `json:"result"`
}
func GetDataFromFile() Sentence {
text, err := os.ReadFile("data.json")
if err != nil {
panic(err)
}
sentenceData := Sentence{}
err = json.Unmarshal(text, &sentenceData)
if err != nil {
panic(err)
}
return sentenceData
}
接下來就是處理資料庫初始化的部份,這邊我們透過gorm來處理資料庫架構的建立與初始化,因為我們在前面有指定struct tag,所以gorm會自動幫我們建立資料庫(這邊以MariaDB為例)
var Database *gorm.DB
const dsn = "root:root@tcp(localhost:3306)/mygo?charset=utf8&parseTime=True&loc=Local"
func CreateDB() *gorm.DB {
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
panic(fmt.Sprintf("failed to connect to database: %v", err))
}
if err := db.AutoMigrate(&SentenceItem{}); err != nil {
panic(fmt.Sprintf("error migrating schema: %v", err))
}
return db
}
接下來我們將資料透過db.Create寫入到資料庫中,透過db.Create可以一次寫入單筆或多筆資料
var sentenceData = GetDataFromFile()
var result = Database.Create(&sentenceData.Result)
if result.Error != nil {
panic(result.Error)
}
db.Create有一個缺點就是如果資料庫中已經有相同的資料,會無法導入(Unique Key),所以我們可以透過針對每一筆資料進行檢查,如果資料庫中已經有相同的資料(基於segment_id),就不進行寫入。
func coorInsert(wg *sync.WaitGroup, item SentenceItem, db *gorm.DB, messageChan chan<- string) {
defer wg.Done()
// Check if the record already exists
var existingItem SentenceItem
err := db.First(&existingItem, "segment_id = ?", item.SegmentId).Error
if err == nil {
messageChan <- fmt.Sprintf("Segment ID(%d) already exists", item.SegmentId)
return
} else if err != gorm.ErrRecordNotFound {
messageChan <- fmt.Sprintf("Error checking Segment ID(%d): %v", item.SegmentId, err)
return
}
// Insert the new record
if err := db.Create(&item).Error; err != nil {
messageChan <- fmt.Sprintf("Error inserting Segment ID(%d): %v", item.SegmentId, err)
return
}
messageChan <- fmt.Sprintf("Successfully inserted Segment ID %d", item.SegmentId)
}
func insertOrUpdate(sentenceData *Sentence, Database *gorm.DB, messageChan chan string, wg *sync.WaitGroup) error {
for _, item := range sentenceData.Result {
wg.Add(1)
go coorInsert(wg, item, Database, messageChan)
}
// wait until all insertItem goroutines are done, then close channel
go func() {
wg.Wait()
close(messageChan)
}()
// dump all messages to stdout
for message := range messageChan {
fmt.Println(message)
}
return nil
}
執行完後檢查資料庫是否有成功寫入資料
SELECT * FROM sentence LIMIT 10;
DESCRIBE sentence;
這邊的插入或更新是透過檢查資料庫中是否已經有相同的資料,如果有就不進行寫入,這邊我們透過goroutine來進行並行寫入,並且透過channel來傳遞訊息,最後再將訊息印出來。
剛開始寫的時候沒有注意到連線數的問題,所以這邊我們透過SHOW VARIABLES LIKE 'max_connections'
來取得最大連線數,並且預留20個連線數給應用程式,這樣就不會因為連線數不足而無法寫入資料。接著初始化一個channel buffer來限制並行寫入的數量,這樣就不會因為太多連線而導致資料庫連線數過高。最後透過sync.WaitGroup
來等待所有goroutine都完成後再關閉channel。
package data
import (
"encoding/json"
"os"
"gorm.io/gorm"
)
// SentenceItem represents a sentence item with GORM model tags
type SentenceItem struct {
ID uint `gorm:"type:int;primaryKey;autoIncrement;not null;"`
Text string `json:"text" gorm:"type:text;not null;"`
Episode string `json:"episode" gorm:"type:varchar(3);not null;"`
FrameStart uint `json:"frame_start" gorm:"type:int;not null;"`
FrameEnd uint `json:"frame_end" gorm:"type:int;not null;"`
SegmentId uint `json:"segment_id" gorm:"type:int;not null;index;unique;"`
}
// TableName sets the insert table name for this struct type
func (SentenceItem) TableName() string {
return "sentence"
}
type Sentence struct {
gorm.Model
Result []SentenceItem `json:"result"`
}
func GetDataFromFile() Sentence {
text, err := os.ReadFile("data.json")
if err != nil {
panic(err)
}
sentenceData := Sentence{}
err = json.Unmarshal(text, &sentenceData)
if err != nil {
panic(err)
}
return sentenceData
}
package data
import (
"fmt"
"strconv"
"sync"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var Database *gorm.DB
const dsn = "ithome:ironman@tcp(localhost:3306)/mygo?charset=utf8&parseTime=True&loc=Local"
func CreateDB() *gorm.DB {
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
panic(fmt.Sprintf("failed to connect to database: %v", err))
}
if err := db.AutoMigrate(&SentenceItem{}); err != nil {
panic(fmt.Sprintf("error migrating schema: %v", err))
}
return db
}
func getMaxConnections() (int, error) {
var value string
row := Database.Raw("SHOW VARIABLES LIKE 'max_connections'").Row()
err := row.Scan(new(string), &value)
if err != nil {
return 0, err
}
maxConnections, err := strconv.Atoi(value)
if err != nil {
return 0, err
}
return maxConnections, nil
}
func coorInsert(wg *sync.WaitGroup, semaphore chan struct{}, item SentenceItem, db *gorm.DB, messageChan chan<- string) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
// Check if the record already exists
var existingItem SentenceItem
err := db.First(&existingItem, "segment_id = ?", item.SegmentId).Error
if err == nil {
messageChan <- fmt.Sprintf("Segment ID(%d) already exists", item.SegmentId)
return
} else if err != gorm.ErrRecordNotFound {
messageChan <- fmt.Sprintf("Error checking Segment ID(%d): %v", item.SegmentId, err)
return
}
// Insert the new record
if err := db.Create(&item).Error; err != nil {
messageChan <- fmt.Sprintf("Error inserting Segment ID(%d): %v", item.SegmentId, err)
return
}
messageChan <- fmt.Sprintf("Successfully inserted Segment ID %d", item.SegmentId)
}
func insertOrUpdate(sentenceData *Sentence, Database *gorm.DB, messageChan chan string, wg *sync.WaitGroup) error {
maxConnections, err := getMaxConnections()
if err != nil {
return fmt.Errorf("failed to get max_connections: %v", err)
}
reservedConnections := 20 // reserve 20 connections for the application
maxConnections -= reservedConnections
if maxConnections <= 0 {
return fmt.Errorf("no available connections for the application")
}
sqlDB, err := Database.DB()
if err != nil {
return fmt.Errorf("failed to get DB instance: %v", err)
}
sqlDB.SetMaxOpenConns(maxConnections)
sqlDB.SetMaxIdleConns(maxConnections)
sqlDB.SetConnMaxLifetime(0)
semaphore := make(chan struct{}, maxConnections) // setup a semaphore to limit concurrency
for _, item := range sentenceData.Result {
wg.Add(1)
go coorInsert(wg, semaphore, item, Database, messageChan)
}
// wait until all insertItem goroutines are done, then close channel
go func() {
wg.Wait()
close(messageChan)
}()
// dump all messages to stdout
for message := range messageChan {
fmt.Println(message)
}
return nil
}
func init() {
messageChan := make(chan string)
var wg sync.WaitGroup
Database = CreateDB()
sentenceData := GetDataFromFile()
err := insertOrUpdate(&sentenceData, Database, messageChan, &wg)
if err != nil {
panic(err)
}
}
那麼今天的文章就到這告一段落,如果我的文章有任何地方有錯誤請在留言區反應
明天將會實作透過FFmpeg輸出指定frame的圖片或GIF
參考資料: